/*
 This file is part of Subsonic.

 Subsonic is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 Subsonic is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with Subsonic.  If not, see <http://www.gnu.org/licenses/>.

 Copyright 2009 (C) Sindre Mehus
 */
package net.nullsum.audinaut.service;

import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.util.Log;

import net.nullsum.audinaut.domain.MusicDirectory;
import net.nullsum.audinaut.util.BufferFile;
import net.nullsum.audinaut.util.CacheCleaner;
import net.nullsum.audinaut.util.Constants;
import net.nullsum.audinaut.util.FileUtil;
import net.nullsum.audinaut.util.SilentBackgroundTask;
import net.nullsum.audinaut.util.Util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import okhttp3.Response;

/**
 * @author Sindre Mehus
 * @version $Id$
 */
public class DownloadFile implements BufferFile {
    private static final String TAG = DownloadFile.class.getSimpleName();
    private static final int MAX_FAILURES = 5;
    private final Context context;
    private final MusicDirectory.Entry song;
    private final File partialFile;
    private final File completeFile;
    private final File saveFile;

    private final MediaStoreService mediaStoreService;
    private final boolean save;
    private final Long contentLength = null;
    private DownloadTask downloadTask;
    private boolean failedDownload = false;
    private int failed = 0;
    private int bitRate;
    private boolean isPlaying = false;
    private boolean saveWhenDone = false;
    private boolean completeWhenDone = false;
    private boolean rateLimit = false;

    public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) {
        this.context = context;
        this.song = song;
        this.save = save;
        saveFile = FileUtil.getSongFile(context, song);
        bitRate = getActualBitrate();
        partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
                ".partial." + FileUtil.getExtension(saveFile.getName()));
        completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
                ".complete." + FileUtil.getExtension(saveFile.getName()));
        mediaStoreService = new MediaStoreService(context);
    }

    public MusicDirectory.Entry getSong() {
        return song;
    }

    public boolean isSong() {
        return song.isSong();
    }

    /**
     * Returns the effective bit rate.
     */
    public int getBitRate() {
        if (!partialFile.exists()) {
            bitRate = getActualBitrate();
        }
        if (bitRate > 0) {
            return bitRate;
        }
        return song.getBitRate() == null ? 160 : song.getBitRate();
    }

    private int getActualBitrate() {
        int br = Util.getMaxBitrate(context);
        if (br == 0 && song.getTranscodedSuffix() != null && "mp3".equals(song.getTranscodedSuffix().toLowerCase())) {
            if (song.getBitRate() != null) {
                br = Math.min(320, song.getBitRate());
            } else {
                br = 320;
            }
        } else if (song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) {
            // If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs)
            if (song.getBitRate() != null && (br == 0 || br > song.getBitRate())) {
                br = song.getBitRate();
            }
        }

        return br;
    }

    public Long getContentLength() {
        return contentLength;
    }

    @Override
    public long getEstimatedSize() {
        if (contentLength != null) {
            return contentLength;
        }

        File file = getCompleteFile();
        if (file.exists()) {
            return file.length();
        } else if (song.getDuration() == null) {
            return 0;
        } else {
            int br = (getBitRate() * 1000) / 8;
            int duration = song.getDuration();
            return br * duration;
        }
    }

    public synchronized void download() {
        rateLimit = false;
        preDownload();
        downloadTask.execute();
    }

    private void preDownload() {
        FileUtil.createDirectoryForParent(saveFile);
        failedDownload = false;
        if (!partialFile.exists()) {
            bitRate = getActualBitrate();
        }
        downloadTask = new DownloadTask(context);
    }

    public synchronized void cancelDownload() {
        if (downloadTask != null) {
            downloadTask.cancel();
        }
    }

    @Override
    public File getFile() {
        if (saveFile.exists()) {
            return saveFile;
        } else if (completeFile.exists()) {
            return completeFile;
        } else {
            return partialFile;
        }
    }

    public File getCompleteFile() {
        if (saveFile.exists()) {
            return saveFile;
        }

        if (completeFile.exists()) {
            return completeFile;
        }

        return saveFile;
    }

    public File getPartialFile() {
        return partialFile;
    }

    public boolean isSaved() {
        return saveFile.exists();
    }

    public synchronized boolean isCompleteFileAvailable() {
        return saveFile.exists() || completeFile.exists();
    }

    @Override
    public synchronized boolean isWorkDone() {
        return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
    }

    @Override
    public void onStart() {
        setPlaying(true);
    }

    @Override
    public void onStop() {
        setPlaying(false);
    }

    @Override
    public synchronized void onResume() {
        if (!isWorkDone() && !isFailedMax() && !isDownloading() && !isDownloadCancelled()) {
            download();
        }
    }

    public synchronized boolean isDownloading() {
        return downloadTask != null && downloadTask.isRunning();
    }

    public synchronized boolean isDownloadCancelled() {
        return downloadTask != null && downloadTask.isCancelled();
    }

    public boolean shouldSave() {
        return save;
    }

    public boolean isFailed() {
        return failedDownload;
    }

    public boolean isFailedMax() {
        return failed > MAX_FAILURES;
    }

    public void delete() {
        cancelDownload();

        // Remove from mediaStore BEFORE deleting file since it calls getCompleteFile
        deleteFromStore();

        // Delete all possible versions of the file
        File parent = partialFile.getParentFile();
        Util.delete(partialFile);
        Util.delete(completeFile);
        Util.delete(saveFile);
        FileUtil.deleteEmptyDir(parent);
    }

    public void unpin() {
        if (saveFile.exists()) {
            // Delete old store entry before renaming to pinned file
            saveFile.renameTo(completeFile);
            renameInStore(saveFile, completeFile);
        }
    }

    public boolean cleanup() {
        boolean ok = true;
        if (completeFile.exists() || saveFile.exists()) {
            ok = Util.delete(partialFile);
        }
        if (saveFile.exists()) {
            ok &= Util.delete(completeFile);
        }
        return ok;
    }

    // In support of LRU caching.
    public void updateModificationDate() {
        updateModificationDate(saveFile);
        updateModificationDate(partialFile);
        updateModificationDate(completeFile);
    }

    private void updateModificationDate(File file) {
        if (file.exists()) {
            boolean ok = file.setLastModified(System.currentTimeMillis());
            if (!ok) {
                Log.w(TAG, "Failed to set last-modified date on " + file);
            }
        }
    }

    public void renamePartial() {
        Util.renameFile(partialFile, completeFile);
        saveToStore();
    }

    public void setPlaying(boolean isPlaying) {
        if (saveWhenDone && !isPlaying) {
            Util.renameFile(completeFile, saveFile);
            renameInStore(completeFile, saveFile);
            saveWhenDone = false;
        } else if (completeWhenDone && !isPlaying) {
            if (save) {
                Util.renameFile(partialFile, saveFile);
                saveToStore();
            } else {
                Util.renameFile(partialFile, completeFile);
                saveToStore();
            }
            completeWhenDone = false;
        }

        this.isPlaying = isPlaying;
    }

    private void deleteFromStore() {
        try {
            mediaStoreService.deleteFromMediaStore(this);
        } catch (Exception e) {
            Log.w(TAG, "Failed to remove from store", e);
        }
    }

    private void saveToStore() {
        if (!Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HIDE_MEDIA, false)) {
            try {
                mediaStoreService.saveInMediaStore(this);
            } catch (Exception e) {
                Log.w(TAG, "Failed to save in media store", e);
            }
        }
    }

    private void renameInStore(File start, File end) {
        try {
            mediaStoreService.renameInMediaStore(start, end);
        } catch (Exception e) {
            Log.w(TAG, "Failed to rename in store", e);
        }
    }

    @Override
    public String toString() {
        return "DownloadFile (" + song + ")";
    }

    // Don't do this.  Causes infinite loop if two instances of same song
    /*@Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        DownloadFile downloadFile = (DownloadFile) o;
        return Util.equals(this.getSong(), downloadFile.getSong());
    }*/

    private class DownloadTask extends SilentBackgroundTask<Void> {
        private MusicService musicService;

        public DownloadTask(Context context) {
            super(context);
        }

        @Override
        public Void doInBackground() throws InterruptedException {
            InputStream in = null;
            FileOutputStream out = null;
            PowerManager.WakeLock wakeLock = null;
            WifiManager.WifiLock wifiLock = null;
            try {

                if (Util.isScreenLitOnDownload(context)) {
                    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                    wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString());
                    wakeLock.acquire();
                }

                wifiLock = Util.createWifiLock(context, toString());
                wifiLock.acquire();

                if (saveFile.exists()) {
                    Log.i(TAG, saveFile + " already exists. Skipping.");
                    checkDownloads();
                    return null;
                }
                if (completeFile.exists()) {
                    if (save) {
                        if (isPlaying) {
                            saveWhenDone = true;
                        } else {
                            Util.renameFile(completeFile, saveFile);
                            renameInStore(completeFile, saveFile);
                        }
                    } else {
                        Log.i(TAG, completeFile + " already exists. Skipping.");
                    }
                    checkDownloads();
                    return null;
                }

                if (musicService == null) {
                    musicService = MusicServiceFactory.getMusicService(context);
                }

                // Some devices seem to throw error on partial file which doesn't exist
                boolean compare;
                try {
                    compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
                } catch (Exception e) {
                    compare = true;
                }
                if (compare) {
                    // Attempt partial HTTP GET, appending to the file if it exists.
                    Response response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
                    if (response.header("Content-Length") != null) {
                        Log.i(TAG, "Content Length: " + contentLength);
                    }

                    boolean partial = response.code() == 206;
                    if (partial) {
                        Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
                    }

                    in = response.body().byteStream();

                    out = new FileOutputStream(partialFile, partial);
                    long n = copy(in, out);
                    Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile);
                    out.flush();
                    out.close();

                    if (isCancelled()) {
                        throw new Exception("Download of '" + song + "' was cancelled");
                    } else if (partialFile.length() == 0) {
                        throw new Exception("Download of '" + song + "' failed.  File is 0 bytes long.");
                    }

                    downloadAndSaveCoverArt(musicService);
                }

                if (isPlaying) {
                    completeWhenDone = true;
                } else {
                    if (save) {
                        Util.renameFile(partialFile, saveFile);
                    } else {
                        Util.renameFile(partialFile, completeFile);
                    }
                    DownloadFile.this.saveToStore();
                }

            } catch (InterruptedException x) {
                throw x;
            } catch (FileNotFoundException x) {
                Util.delete(completeFile);
                Util.delete(saveFile);
                if (!isCancelled()) {
                    failed = MAX_FAILURES + 1;
                    failedDownload = true;
                    Log.w(TAG, "Failed to download '" + song + "'.", x);
                }
            } catch (IOException x) {
                Util.delete(completeFile);
                Util.delete(saveFile);
                if (!isCancelled()) {
                    failedDownload = true;
                    Log.w(TAG, "Failed to download '" + song + "'.", x);
                }
            } catch (Exception x) {
                Util.delete(completeFile);
                Util.delete(saveFile);
                if (!isCancelled()) {
                    failed++;
                    failedDownload = true;
                    Log.w(TAG, "Failed to download '" + song + "'.", x);
                }
            } finally {
                Util.close(in);
                Util.close(out);
                if (wakeLock != null) {
                    wakeLock.release();
                    Log.i(TAG, "Released wake lock " + wakeLock);
                }
                if (wifiLock != null) {
                    wifiLock.release();
                }
            }

            // Only run these if not interrupted, ie: cancelled
            DownloadService downloadService = DownloadService.getInstance();
            if (downloadService != null && !isCancelled()) {
                new CacheCleaner(context, downloadService).cleanSpace();
                checkDownloads();
            }

            return null;
        }

        private void checkDownloads() {
            DownloadService downloadService = DownloadService.getInstance();
            if (downloadService != null) {
                downloadService.checkDownloads();
            }
        }

        @Override
        public String toString() {
            return "DownloadTask (" + song + ")";
        }

        private void downloadAndSaveCoverArt(MusicService musicService) {
            try {
                if (song.getCoverArt() != null) {
                    // Check if album art already exists, don't want to needlessly load into memory
                    File albumArtFile = FileUtil.getAlbumArtFile(context, song);
                    if (!albumArtFile.exists()) {
                        musicService.getCoverArt(context, song, 0, null, null);
                    }
                }
            } catch (Exception x) {
                Log.e(TAG, "Failed to get cover art.", x);
            }
        }

        private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException {

            // Start a thread that will close the input stream if the task is
            // cancelled, thus causing the copy() method to return.
            new Thread("DownloadFile_copy") {
                @Override
                public void run() {
                    while (true) {
                        Util.sleepQuietly(3000L);
                        if (isCancelled()) {
                            Util.close(in);
                            return;
                        }
                        if (!isRunning()) {
                            return;
                        }
                    }
                }
            }.start();

            byte[] buffer = new byte[1024 * 16];
            long count = 0;
            int n;
            long lastLog = System.currentTimeMillis();
            long lastCount = 0;

            boolean activeLimit = rateLimit;
            while (!isCancelled() && (n = in.read(buffer)) != -1) {
                out.write(buffer, 0, n);
                count += n;
                lastCount += n;

                long now = System.currentTimeMillis();
                if (now - lastLog > 3000L) {  // Only every so often.
                    Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
                    lastLog = now;
                    lastCount = 0;

                    // Re-establish every few seconds whether screen is on or not
                    if (rateLimit) {
                        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                        activeLimit = pm.isScreenOn();
                    }
                }

                // If screen is on and rateLimit is true, stop downloading from exhausting bandwidth
                if (activeLimit) {
                    Thread.sleep(10L);
                }
            }
            return count;
        }
    }
}
